using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Magiq.Tests.Support;
using Magiq.Tests.TestModel;
using Magiq.Update;
using NUnit.Framework;

namespace Magiq.Tests {
  public abstract class BaseMagiqProviderUpdatesTest : BaseMagiqProviderTest {
    [Test]
    public void ShouldUpdateStringProperties() {
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.StringProperty, "q"),
                 x => x.StringProperty == "q");
    }

    [Test]
    public void ShouldUpdateIntProperties() {
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.IntProperty, 3),
                 x => x.IntProperty == 3);
    }

    [Test]
    public void ShouldUpdateReferencesAsValues() {
      var r = TestClass2s.First();
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.Reference, r),
                 x => x.Reference == r);
    }

    [Test]
    public void ShouldUpdateChainedReferences() {
      TestUpdate(TestClass1s,
                 x => x.StringProperty == "sarasa",
                 s => s.Set(x => x.Reference.StringProperty, "lala"),
                 items => items.Select(x => x.Reference).Distinct().Count(),
                 x => x.Reference != null && x.Reference.StringProperty == "lala");
    }

    [Test]
    public void ShouldUpdateSelfReferences() {
      TestUpdate(TestClass1s,
                 x => x.StringProperty == "sarasa",
                 s => s.Set(x => x.SelfReference.StringProperty, "self changed"),
                 items => items.Where(x=>x.SelfReference != null).Select(x => x.SelfReference).Distinct().Count(),
                 x => x.SelfReference != null && x.SelfReference.StringProperty == "self changed");
    }

    [Test]
    public void ShouldUpdateWhereThatUsesSelfReference() {
      TestUpdate(TestClass1s, 
                 x => x.SelfReference != null && x.SelfReference.IntProperty > 3,
                 x => x.Set(u => u.StringProperty, "lala"),
                 x => x.StringProperty == "lala");
    }

    [Test]
    public void ShouldUpdateChainedReferencesWithOneSingleSetThatInvalidatesTheWhereClause() {
      TestUpdate(TestClass1s,
                 x => x.StringProperty == "sarasa",
                 s => s.Set(x => x.StringProperty, "lolo")
                        .Set(x => x.Reference.StringProperty, "lala"),
                 items => items.Select(x => x.Reference).Distinct().Count() + items.Count());
    }

    [Test]
    public void ShouldUpdateChainedReferencesWithSeveralSetsThatInvalidatesTheWhereClause() {
      TestUpdate(TestClass1s,
                 x => x.StringProperty == "sarasa" && x.IntProperty == 3,
                 s => s.Set(x => x.StringProperty, "lolo")
                        .Set(x => x.IntProperty, 5)
                        .Set(x => x.Reference.StringProperty, "lala"),
                 items => items.Select(x => x.Reference).Distinct().Count() + items.Count());
    }

    [Test]
    public void ShouldUpdateReferencesAsExpressions() {
      var r = TestClass2s.First();
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.Reference, x => r),
                 x => x.Reference == r);
    }


    [Test]
    public void ShouldUpdateReferenceEqualToSourceReference() {
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.Reference, x => x.Reference),
                 x => true);
    }


    [Test]
    public void ShouldUpdateReferenceEqualToAnotherSourceReference() {
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.Reference2, x => x.Reference),
                 x => true);
    }

    [Test]
    public void ShouldUpdateSeveralProperties() {
      var r = TestClass2s.First();
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.IntProperty, 3)
                        .Set(x => x.StringProperty, "lalala")
                        .Set(x => x.Reference, r),
                 x => x.Reference == r && x.IntProperty == 3 && x.StringProperty == "lalala");
    }

    [Test]
    public void ShouldUpdateSeveralEntities() {
      var items = TestClass1s.Where(x => x.StringProperty == "sarasa");
      var count = items.Select(x => x.Reference).Distinct().Count() + items.Count();
      items.Set(x => x.IntProperty, 3)
        .Set(x => x.AnotherStringProperty, "pe")
        .Set(x => x.Reference.StringProperty, "lala")
        .Update().Should(Be.EqualTo(count));
    }

    [Test]
    public void ShouldUpdateWhereThatUsesStringProperty() {
      TestUpdate(TestClass1s,
                 x => x.StringProperty == "sarasa",
                 x => x.Set(u => u.StringProperty, "lala"),
                 x => x.StringProperty == "lala");
    }

    [Test]
    public void ShouldUpdateComplexWhere() {
      TestUpdate(TestClass1s,
                 x =>
                 (x.StringProperty == "sarasa" && x.IntProperty >= 3) ||
                 x.AnotherIntProperty < 12 && x.AnotherStringProperty != null,
                 x => x.Set(u => u.StringProperty, "lala"),
                 x => x.StringProperty == "lala");
    }

    [Test]
    public void ShouldUpdateWhereThatUsesAnotherStringProperty() {
      TestUpdate(TestClass1s,
                 x => x.StringProperty == x.AnotherStringProperty,
                 x => x.Set(u => u.StringProperty, "lala"),
                 x => x.StringProperty == "lala");
    }


    [Test]
    public void ShouldUpdateWhereThatUsesStringPropertyNull() {
      TestUpdate(TestClass1s,
                 x => x.StringProperty == null,
                 x => x.Set(u => u.StringProperty, "lala"),
                 x => x.StringProperty == "lala");
    }

    [Test]
    public void ShouldUpdateWhereThatUsesComposedStringProperty() {
      TestUpdate(TestClass1s,
                 x => x.StringProperty == x.AnotherStringProperty + "la",
                 x => x.Set(u => u.StringProperty, "lala"),
                 x => x.StringProperty == "lala");
    }

    [Test]
    public void ShouldUpdateWithComposedStringProperty() {
      TestUpdate(TestClass1s,
                 x => x.StringProperty == "sarasa",
                 s => s.Set(x => x.StringProperty, x => x.StringProperty + "lala"),
                 x => x.StringProperty == "sarasalala");
    }

    [Test]
    public void ShouldUpdateWithComposedStringProperties() {
      TestUpdate(TestClass1s,
                 x => x.StringProperty == "sarasa",
                 s => s.Set(x => x.StringProperty, x => x.StringProperty + x.AnotherStringProperty),
                 x => x.StringProperty == "sarasa" + x.AnotherStringProperty);
    }

    [Test]
    public void ShouldUpdateFromAnotherStringProperty() {
      TestUpdate(TestClass1s,
                 x => x.StringProperty == "sarasa",
                 s => s.Set(x => x.StringProperty, x => x.AnotherStringProperty),
                 x => x.StringProperty == x.AnotherStringProperty);
    }

    [Test]
    public void ShouldUpdateWithComposedIntProperty() {
      TestUpdate(TestClass1s,
                 x => x.IntProperty == 3,
                 s => s.Set(x => x.IntProperty, x => x.IntProperty + 5),
                 x => x.IntProperty == 8);
    }

    [Test]
    public void ShouldUpdateWithComposedIntProperties() {
      TestUpdate(TestClass1s,
                 x => x.IntProperty == 3,
                 s => s.Set(x => x.IntProperty, x => x.IntProperty + x.AnotherIntProperty),
                 x => x.IntProperty == 3 + x.AnotherIntProperty);
    }

    [Test]
    public void ShouldUpdateFromAnotherIntProperty() {
      TestUpdate(TestClass1s,
                 x => x.IntProperty == 3,
                 s => s.Set(x => x.IntProperty, x => x.AnotherIntProperty),
                 x => x.IntProperty == x.AnotherIntProperty);
    }

    [Test]
    public void ShouldUpdateWhereThatUsesIntProperty() {
      var items = TestClass1s.Where(x => x.IntProperty == 3);
      var count = items.Count();
      items.Set(x => x.IntProperty, 4).Update().Should(Be.EqualTo(count));
    }

    [Test]
    public void ShouldUpdateWhereThatUsesComposedIntProperty() {
      var items = TestClass1s.Where(x => x.IntProperty == x.AnotherIntProperty + 3);
      var count = items.Count();
      items.Set(x => x.IntProperty, 4).Update().Should(Be.EqualTo(count));
    }

    [Test]
    public void ShouldUpdateWhereThatUsesAnotherIntProperty() {
      var items = TestClass1s.Where(x => x.IntProperty == x.AnotherIntProperty);
      var count = items.Count();
      items.Set(x => x.IntProperty, 4).Update().Should(Be.EqualTo(count));
    }

    [Test]
    public void ShouldUpdateWhereThatUsesReference() {
      var r = TestClass2s.First();
      var items = TestClass1s.Where(x => x.Reference == r);
      var count = items.Count();
      items.Set(x => x.Reference, r).Update().Should(Be.EqualTo(count));
    }

    [Test]
    public void ShouldUpdateWhereThatUsesReferenceNull() {
      var r = TestClass2s.First();
      var items = TestClass1s.Where(x => x.Reference == null);
      var count = items.Count();
      items.Set(x => x.Reference, x => r).Update().Should(Be.EqualTo(count));
    }

    [Test]
    public void ShouldUpdateNullToReferenceAsExpressions() {
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.Reference, x => null),
                 x => x.Reference == null);
    }

    [Test]
    public void ShouldUpdateNullToReferenceAsValues() {
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.Reference, (TestClass2) null),
                 x => x.Reference == null);
    }

    [Test]
    public void ShouldUpdateNullToStringProperty() {
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.StringProperty, x => null),
                 x => x.StringProperty == null);
    }

    [Test]
    public void ShouldUpdateNullToNullableIntProperty() {
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.NullableIntProperty, x => null),
                 x => x.NullableIntProperty == null);
    }

    [Test]
    public void ShouldUpdateLongToIntProperty() {
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.IntProperty, 3L),
                 x => x.IntProperty == 3);
    }

    [Test]
    public void ShouldUpdateByteToIntProperty() {
      const byte value = 3;
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.IntProperty, value),
                 x => x.IntProperty == 3);
    }

    [Test]
    public void ShouldUpdateNullableIntProperties() {
      TestUpdate(TestClass1s,
                 s => s.Set(x => x.NullableIntProperty, 3),
                 x => x.NullableIntProperty == 3);
    }

    [Test]
    public void ShouldUpdateWithComposedNullableIntProperty() {
      TestUpdate(TestClass1s,
                 x => x.NullableIntProperty == 3,
                 s => s.Set(x => x.NullableIntProperty, x => x.NullableIntProperty + 5),
                 x => x.NullableIntProperty == 8);
    }

    [Test]
    public void ShouldUpdateWithComposedNullableIntProperties() {
      TestUpdate(TestClass1s,
                 x => x.NullableIntProperty == 3,
                 s => s.Set(x => x.IntProperty, x => x.NullableIntProperty + x.AnotherIntProperty),
                 x => x.IntProperty == 3 + x.AnotherIntProperty);
    }

    [Test]
    public void ShouldUpdateNullableFromAnotherIntProperty() {
      TestUpdate(TestClass1s,
                 x => x.IntProperty == 3,
                 s => s.Set(x => x.NullableIntProperty, x => x.AnotherIntProperty),
                 x => x.NullableIntProperty == x.AnotherIntProperty);
    }

    [Test]
    public void ShouldUpdateWhereThatUsesNullableIntProperty() {
      var items = TestClass1s.Where(x => x.NullableIntProperty == 3);
      var count = items.Count();
      items.Set(x => x.NullableIntProperty, 4).Update().Should(Be.EqualTo(count));
    }

    [Test]
    public void ShouldUpdateWhereThatUsesNullableIntPropertyNull() {
      var items = TestClass1s.Where(x => x.NullableIntProperty == null);
      var count = items.Count();
      items.Set(x => x.NullableIntProperty, 4).Update().Should(Be.EqualTo(count));
    }

    [Test]
    public void ShouldUpdateWhereThatUsesNullableIntPropertyWithIntProperty() {
      var items = TestClass1s.Where(x => x.NullableIntProperty == x.IntProperty);
      var count = items.Count();
      items.Set(x => x.NullableIntProperty, 4).Update().Should(Be.EqualTo(count));
    }


    [Test]
    public void ShouldUpdateWhereThatUsesNullableIntPropertyToNull() {
      var items = TestClass1s.Where(x => x.NullableIntProperty == null);
      var count = items.Count();
      items.Set(x => x.NullableIntProperty, 4).Update().Should(Be.EqualTo(count));
    }

    protected static void TestUpdate<T>(IQueryable<T> queryable, Func<IQueryable<T>, IUpdate<T>> sets,
                                        Expression<Func<T, bool>> updatedWhere) {
      TestUpdate(queryable, x => true, sets, updatedWhere);
    }

    protected static void TestUpdate<T>(IQueryable<T> queryable, Expression<Func<T, bool>> where,
                                        Func<IQueryable<T>, IUpdate<T>> sets, Expression<Func<T, bool>> updatedWhere) {
      TestUpdate(queryable, where, sets, items => items.Distinct().Count(), updatedWhere);
    }

    protected static int TestUpdate<T>(IQueryable<T> queryable, Expression<Func<T, bool>> where,
                                       Func<IQueryable<T>, IUpdate<T>> sets, Func<IEnumerable<T>, int> getCount) {
      var items = queryable.Where(where);
      var count = getCount(items);
      sets(queryable.Where(where)).Update().Should(Be.EqualTo(count));
      return count;
    }

    protected static void TestUpdate<T>(IQueryable<T> queryable, Expression<Func<T, bool>> where,
                                        Func<IQueryable<T>, IUpdate<T>> sets, Func<IEnumerable<T>, int> getCount,
                                        Expression<Func<T, bool>> updatedWhere) {
      var count = TestUpdate(queryable, where, sets, getCount);
      queryable.Where(updatedWhere).Count().Should(Be.GreaterThanOrEqualTo(count));
    }
  }
}